from collections import deque
import copy
import random
import re


class Directions:
    north = 0
    northeast = 1
    east = 2
    southeast = 3
    south = 4
    southwest = 5
    west = 6
    northwest = 7
    coords = [
        (0, -1),
        (1, -1),
        (1, 0),
        (1, 1),
        (0, 1),
        (-1, 1),
        (-1, 0),
        (-1, -1)
    ]


def is_solid(obj):
    if not obj:
        return False
    return obj.is_solid


class DeltaColor:
    """Represents difference between colors."""
    def __init__(self, r=0, g=0, b=0, a=0):
        self.r = r
        self.g = g
        self.b = b
        self.a = a

    def __add__(self, other):
        if isinstance(other, DeltaColor):
            new_color = copy.deepcopy(self)
            new_color.r += other.r
            new_color.g += other.g
            new_color.b += other.b
            new_color.a += other.a
            return new_color
        elif isinstance(other, Color):
            return other + self
        raise TypeError("Unsupported opperation for types " + str(
            type(self)) + " and" + str(type(other)))

    def __sub__(self, other):
        if isinstance(other, DeltaColor):
            return self + (-other)
        raise TypeError("Unsupported opperation for types " + str(
            type(self)) + " and" + str(type(other)))

    def __neg__(self):
        new_color = copy.deepcopy(self)
        new_color.r = -self.r
        new_color.g = -self.g
        new_color.b = -self.b
        new_color.a = -self.a
        return new_color

    def __abs__(self):
        new_color = copy.deepcopy(self)
        new_color.r = abs(self.r)
        new_color.g = abs(self.g)
        new_color.b = abs(self.b)
        new_color.a = abs(self.a)
        return new_color

    def __mul__(self, other):
        new_color = copy.deepcopy(self)
        new_color.r = self.r * other
        new_color.g = self.g * other
        new_color.b = self.b * other
        new_color.a = self.a * other
        return new_color

    def __div__(self, other):
        return self * (1 / other)

    def __str__(self):
        return type(self).__name__ + str((self.r, self.g, self.b, self.a))

    def __repr__(self):
        return str(self)


def lerp_colors(start, stop, amount):
    """Linear interpolation of two colors (only r,g,b components, a is
        as start.a).
    start - start color
    stop - end color
    amount - numbe between 0 and 1 that represents which point is being computed
    """
    new_color = copy.deepcopy(start)
    new_color.r = start.r * (1 - amount) + stop.r * amount
    new_color.g = start.g * (1 - amount) + stop.g * amount
    new_color.b = start.b * (1 - amount) + stop.b * amount
    return new_color


def color_from_hex(hexadecimal):
    """Make Color object from hexadecimal representation that can be:
        0xAARRGGBB
        0xRRGGBB
        #AARRGGBB
        #RRGGBB
        AARRGGBB
        RRGGBB
    """
    pattern = re.compile(r"""
        (?:\#|(?:0x))? # indicator of hexadecimal string
        (?P<a>[0-9a-fA-F][0-9a-fA-F])? # alpha value
        (?P<r>[0-9a-fA-F][0-9a-fA-F])  # red value
        (?P<g>[0-9a-fA-F][0-9a-fA-F])  # green value
        (?P<b>[0-9a-fA-F][0-9a-fA-F])  # blue value
        """, re.X)
    match = re.match(pattern, hexadecimal)
    try:
        color_dict = match.groupdict()
    except AttributeError:
        raise ValueError("String is in wrong format.")
    if color_dict["a"] is None:
        color_dict["a"] = "ff"
    for k, v in color_dict.items():
        color_dict[k] = int(v, 16)
    return Color(color_dict["r"], color_dict["g"], color_dict["b"], color_dict["a"])


class Color:
    """UDoC class for representing color."""
    def __init__(self, r=0, g=0, b=0, a=255):
        self._colors = [r, g, b]
        self._a = a

    def desaturate(self, amount=1):
        """Desatures this color."""
        average = sum(self._colors) // 3
        self._colors = [average * amount + val * (1 - amount)
                        for val in self._colors]

    def desaturated(self, amount=1):
        """Returns desaturated version of this color."""
        new_color = copy.deepcopy(self)
        new_color.desaturate(amount)
        return new_color

    def randomize(self, max_offset, rnd=random):
        """Randomizes all components of this color by random value (up to max_offset)
            using rnd as random.Random object for generating."""
        self.r = self.r + rnd.randint(-max_offset, max_offset)
        self.g = self.g + rnd.randint(-max_offset, max_offset)
        self.b = self.b + rnd.randint(-max_offset, max_offset)

    def randomized(self, max_offset, rnd=random):
        """Returns randomized version of this color made by randomize."""
        new_color = copy.deepcopy(self)
        new_color.randomize(max_offset, rnd)
        return new_color

    def overlayed(self, other):
        """Overlays this color with another."""
        amount = ((1 - self.a_fl) * (1 - other.a_fl) + other.a_fl)
        # Previous line is probably wrong, but hopefully good enough.
        if self.a == 255:
            amount = other.a_fl
        new_color = lerp_colors(self, other, amount)
        new_color.a = (1 - ((1 - self.a_fl) * (1 - other.a_fl))) * 255
        return new_color

    def __add__(self, other):
        if isinstance(other, DeltaColor):
            new_color = copy.deepcopy(self)
            new_color.r = self.r + other.r
            new_color.g = self.g + other.g
            new_color.b = self.b + other.b
            new_color.a = self.a + other.a
            return new_color
        raise TypeError("Unsupported opperation for types " + str(
            type(self)) + " and" + str(type(other)))

    def __sub__(self, other):
        if isinstance(other, Color):
            new_color = DeltaColor(self.r - other.r,
                                   self.g - other.g,
                                   self.b - other.b,
                                   self.a - other.a)
            return new_color
        if isinstance(other, DeltaColor):
            return self + (-other)
        raise TypeError("Unsupported opperation for types " + str(
            type(self)) + " and" + str(type(other)))

    @property
    def r(self):
        """Red component."""
        return self._colors[0]

    @r.setter
    def r(self, value):
        self._colors[0] = max(0, min(255, int(value)))

    @property
    def g(self):
        """Green component."""
        return self._colors[1]

    @g.setter
    def g(self, value):
        self._colors[1] = max(0, min(255, int(value)))

    @property
    def b(self):
        """Blue component."""
        return self._colors[2]

    @b.setter
    def b(self, value):
        self._colors[2] = max(0, min(255, int(value)))

    @property
    def a(self):
        return self._a

    @a.setter
    def a(self, value):
        self._a = max(0, min(255, int(value)))

    @property
    def a_fl(self):
        """Floating representation of a between 0 and 1."""
        return self.a / 255

    @property
    def hex(self):
        """Return color in hex formatted string 0xAARRGGBB. If a is 255, returns
            it as 0xRRGGBB instead."""
        if self.a == 255:
            return "0x{0:02x}{1:02x}{2:02x}".format(self.r, self.g, self.b)
        return "0x{0:02x}{1:02x}{2:02x}{3:02x}".format(self.a, self.r, self.g, self.b)

    def __str__(self):
        return type(self).__name__ + str((self.r, self.g, self.b, self.a))

    def __repr__(self):
        return str(self)


class RenderRepr:
    """Representation of tile for rendering."""
    def __init__(self, char, fg=None, bg=None, hidden_version=None):
        """
        char - string of length 1 that will by printed
        bg - background of char, either (r, g, b) tuple of 0-255 ints,
            hexadecimal representation, None for transparent or Color object()
        fg - foreground of char
        hidden_version - RenderRepr of this tile in hidden (remembered) state,
            if None, bottom layer's will be used
        """
        if isinstance(bg, Color):
            pass
        elif bg is None:
            bg = Color(a=0)
        elif isinstance(bg, tuple):
            bg = Color(*bg)
        elif isinstance(bg, str):
            bg = color_from_hex(bg)
        if isinstance(fg, Color):
            pass
        elif fg is None:
            fg = Color(a=0)
        elif isinstance(fg, tuple):
            fg = Color(*fg)
        elif isinstance(fg, str):
            fg = color_from_hex(fg)
        self.properties = {"char": char, "bg": bg, "fg": fg,
                           "hidden_version": hidden_version}
        self.other_states = deque()

    def join(self, bg_render_repr):
        """Makes this RenderRepr inherit all values that are nonexistent or
            transparent from bg_render_repr.
        """
        for key, value in self.properties.items():
            if value is None:
                self.properties[key] = copy.deepcopy(
                    bg_render_repr.properties[key])
            if isinstance(value, Color) and value.a < 255:
                self.properties[key] = \
                    bg_render_repr.properties[key].overlayed(value)

    def join_no_partial_transparency(self, bg_render_repr):
        for key, value in self.properties.items():
            if value is None or (isinstance(value, Color) and value.a != 255):
                self.properties[key] = copy.deepcopy(
                    bg_render_repr.properties[key])

    def all_state_join(self, bg_render_repr):
        """Applies join to all states."""
        for i in range(len(self)):
            self.join(bg_render_repr)
            self.cycle_states()

    def add_state(self, state):  # TODO: Add support for timed states.
        """Adds another RenderRepr to this one which will be cycled each few
            frames.
        """
        self.other_states.appendleft(copy.deepcopy(state.properties))
        q = copy.copy(state.other_states)
        while not q.empty():
            self.other_states.appendleft(copy.deepcopy(q.get()))

    def cycle_states(self):
        """Cycles the next state for this RenderRepr."""
        self.other_states.appendleft(self.properties)
        self.properties = self.other_states.pop()

    def __getattr__(self, name):
        if not "properties" in self.__dict__:
            self.__dict__["properties"] = {}
        if name not in self.__dict__["properties"]:
            raise AttributeError(name + " not found.")
        else:
            return self.properties[name]

    def __setattr__(self, name, value):
        if name == "properties":
            self.__dict__["properties"] = value
        if name in self.properties:
            if name == "bg" or name == "fg":
                if value is None:
                    value = Color(transparent=True)
                if isinstance(value, tuple):
                    value = Color(*value)
            self.properties[name] = value
        else:
            self.__dict__[name] = value

    def __iter__(self):
        return iter([self.properties] + list(self.other_states))

    def __len__(self):
        return len(self.other_states) + 1


def default_hidden_version(render_repr):
    """Return default hidden version of render_repr."""
    new_repr = copy.deepcopy(render_repr)
    new_repr.fg.desaturate(0.6)
    new_repr.bg.desaturate(0.6)
    new_repr.fg -= DeltaColor(30, 30, 30)
    new_repr.bg -= DeltaColor(30, 30, 30)
    return new_repr


def get_tile_render_repr(tile):  # Nice but slow.
    """Generate final RenderRepr for tile."""
    render_repr = copy.deepcopy(no_render_repr)
    first_layer_added = False
    for layer_name in tile.layer_render_priority:
        layer = tile[layer_name]
        # I am making an assumption that there won't be multiple backgrounds
        if first_layer_added:
            if len(layer) > 0:
                render_repr.all_state_join(layer.get_some().render_repr)
        elif len(layer) > 0:
            for i, obj in enumerate(layer):
                if i == 0:
                    render_repr.join(obj.render_repr)
                else:
                    render_repr.add_state(obj.render_repr)
            first_layer_added = True
    return render_repr


def get_tile_render_repr_fast(tile):  # Ugly but fast.
    """Generate final RenderRepr for tile.
        Fast but ignores cycling, partial transparency and transparent
        foregrounds and chars.
    """
    render_repr = None
    first_layer_added = False
    for layer_name in tile.layer_render_priority:
        layer = tile[layer_name]
        if not layer.is_empty():
            if first_layer_added:
                render_repr.join_no_partial_transparency(
                    layer.get_some().render_repr)
                render_repr.hidden_version.join_no_partial_transparency(
                    layer.get_some().render_repr.hidden_version)
            else:
                render_repr = layer.get_some().render_repr
                first_layer_added = True
            if render_repr.bg is not None and render_repr.bg.a == 255 and render_repr.hidden_version is not None:
                break
    return render_repr
black_render_repr = RenderRepr(" ", (0, 0, 0), (0, 0, 0))
no_render_repr = RenderRepr(None, None, None)

# Libtcod key list:
# [b for a,b in sorted([(v,k) for k,v in libtcod.__dict__.items()
#   if k.startswith("KEY_")])]


class Key:
    # Add information whether it is pressed or released?
    SPECIAL_KEYS = [
        "none", "escape", "backspace", "tab", "enter", "shift",
        "ctrl", "alt", "pause", "capslock", "pageup", "pagedown",
        "end", "home", "up", "left", "right", "down", "printscreen",
        "insert", "delete", "l_win", "r_win", "apps", "0", "1", "2",
        "3", "4", "5", "6", "7", "8", "9", "kp_0", "kp_1", "kp_2",
        "kp_3", "kp_4", "kp_5", "kp_6", "kp_7", "kp_8", "kp_9",
        "kp_+", "kp_-", "kp_/", "kp_*", "kp_.", "kp_enter", "f1",
        "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10",
        "f11", "f12", "numlock", "scrolllock", " "]

    def __init__(self, char, lctrl=False, rctrl=False, lalt=False, ralt=False, shift=False, ctrl=False, alt=False):
        self.char = char.lower()
        self.flags = set()
        self.other_flags = set()
        if lctrl:
            self.other_flags.add("lctrl")
            self.flags.add("ctrl")
        if rctrl:
            self.other_flags.add("rctrl")
            self.flags.add("ctrl")
        if lalt:
            self.other_flags.add("lalt")
            self.flags.add("alt")
        if ralt:
            self.other_flags.add("ralt")
            self.flags.add("alt")
        if shift:
            self.flags.add("shift")
        if ctrl:
            self.flags.add("ctrl")
        if alt:
            self.flags.add("alt")

    def __str__(self):
        return self.char

    def __hash__(self):
        hash_list = [self.char]
        if "shift" in self.flags:
            hash_list.append("shift")
        if "alt" in self.flags:
            hash_list.append("alt")
        if "ctrl" in self.flags:
            hash_list.append("ctrl")
        hash_list.reverse()
        return hash("+".join(hash_list))

    def __eq__(self, other):
        if not isinstance(other, Key):
            return False
        if other.char != self.char:
            return False
        if other.flags != self.flags:
            return False
        return True

    def __contains__(self, item):
        return item in self.flags or item in self.other_flags


def expand_tuple_keys(dictionary):
    expanded = {}
    for k, v in dictionary.items():
        if isinstance(k, tuple):
            for i in k:
                expanded[i] = v
        else:
            expanded[k] = v
    return expanded
